package br.mikhas.container.annotation;

import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import br.mikhas.container.config.BeanDefinition;
import br.mikhas.container.config.BeanDefinitionProvider;
import br.mikhas.util.Assert;
import br.mikhas.util.ClassScanner;

/**
 * Provides to an annotated context bean definition by scanning the classes on
 * defines providers.
 * 
 * @author Mikhail Domanoski
 * 
 */
public class AnnotatedBeanProvider implements BeanDefinitionProvider {

	/**
	 * The packages to be scanned by the bean provider
	 */
	private List<String> packages = new ArrayList<String>();

	/**
	 * Scanner used to get the classes from the packages.
	 */
	private ClassScanner classScanner = new ClassScanner();

	/**
	 * The stereotype annotations to filter the classes
	 */
	private Set<Class<? extends Annotation>> stereotypes = new HashSet<Class<? extends Annotation>>();

	private AnnotatedBeanDefinitionExtractor extractor;

	private List<BeanDefinition> definitions;

	public AnnotatedBeanProvider() {
		extractor = new AnnotatedBeanDefinitionExtractor(this);
	}

	/**
	 * Adds a package to be scanned
	 * 
	 * @param pack
	 *            an available class package
	 */
	public void addScannedPackage(String pack) {
		packages.add(pack);
	}

	/**
	 * Adds packages to be scanned
	 * 
	 * @param packs
	 *            The packages to be scanned
	 */
	public void addScannedPackages(String... packs) {
		packages.addAll(Arrays.asList(packs));
	}

	private boolean extracting = false;

	@Override
	public List<BeanDefinition> listResourceDefinition() {
		if (extracting)
			throw new IllegalStateException(
					"This provider is already on listing process.");

		definitions = new ArrayList<BeanDefinition>();
		extracting = true;

		List<Class<?>> beanClasses = this.getResourceClasses();

		List<BeanDefinition> beanDefinitions = this
				.extractResourceDefinition(beanClasses);

		beanDefinitions.addAll(this.definitions);

		definitions = null;
		extracting = false;

		return beanDefinitions;
	}

	/**
	 * Scans the desired packages and return the elegible annotated beans.
	 * 
	 * @return
	 */
	private List<Class<?>> getResourceClasses() {
		List<Class<?>> classes = new ArrayList<Class<?>>(300);

		for (String pack : this.packages) {
			List<Class<?>> scanned = this.classScanner.scan(pack);

			Iterator<Class<?>> iterator = scanned.iterator();

			while (iterator.hasNext()) {
				Class<?> cls = iterator.next();
				if (isAnnotatedResource(cls)) {
					classes.add(cls);
				} else {
					iterator.remove();
				}
			}
		}
		return classes;
	}

	/**
	 * Checks if the class is an elegible annotated bean
	 * 
	 * @param beanClass
	 *            A scanned annotated class
	 * @return <code>true<code> if the class is an annotated bean class
	 */
	private boolean isAnnotatedResource(Class<?> beanClass) {
		Annotation[] annotations = beanClass.getAnnotations();
		for (Annotation ann : annotations) {

			if (this.stereotypes.contains(ann.annotationType())) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Adds an stereotype annotation to filter the bean classes
	 * 
	 * @param stereotype
	 *            Annotation which defines a bean stereotype
	 */
	public void addScannedStereotype(Class<? extends Annotation> stereotype) {
		Assert.notNull(stereotype);
		stereotypes.add(stereotype);
	}

	/**
	 * Extract the bean definition from a list bean classes.
	 * 
	 * @param beanClasses
	 *            the bean classes
	 * @return the bean definition for the bean classes
	 */
	private List<BeanDefinition> extractResourceDefinition(
			List<Class<?>> beanClasses) {

		List<BeanDefinition> definitions = new ArrayList<BeanDefinition>(
				beanClasses.size());

		for (Class<?> cls : beanClasses) {
			BeanDefinition definition = this.extractBeanDefinition(cls);

			if (definition != null)
				definitions.add(definition);
		}

		return definitions;
	}

	/**
	 * Extract the bean definition from an annotated bean class
	 * 
	 * @param beanClass
	 * @return
	 */
	private BeanDefinition extractBeanDefinition(Class<?> beanClass) {
		return extractor.extractDefinition(beanClass);
	}

	void addDefinitionExtractor(ChainedDefinitionExtractor extractor) {
		this.extractor.addExtractor(extractor);
	}

	void addBeanDefinition(BeanDefinition definition) {
		if (definitions != null)
			definitions.add(definition);
	}
}
